<?php

declare(strict_types=1);

namespace core;

use RuntimeException;
use InvalidArgumentException;

/**
 * 控制器基类
 *
 * 控制器都应继承此类
 * 它负责接收并存储核心应用程序实例
 * 使子类能够访问应用程序的核心功能和数据
 */
abstract class Controller
{
    /* @var App 应用程序核心实例 */
    protected App $app;

    /** @var Database */
    protected Database $db;

    /** @var View */
    protected View $view;

    /**
     * 注入 App 实例。所有继承此类的子控制器如果定义了自己的构造函数，
     * 必须调用 `parent::__construct($app)` 以确保依赖注入成功。
     *
     * @param App $app 应用程序核心实例
     */
    public function __construct(App $app, View $view, Database $db)
    {
        $this->app  = $app;
        $this->view = $view;
        $this->db   = $db;
    }

    /**
     * 获取配置值
     *
     * @param string|null $key
     * @param mixed $default
     * @return mixed
     */
    final protected function config(?string $key = null, mixed $default = null): mixed
    {
        return $this->app->config($key, $default);
    }

    /**
     * 渲染视图快捷方法
     */
    protected function display(string $view, array $data = []): void
    {
        $this->view->display($view, $data);
    }

    /**
     * 渲染并包含指定路径的视图文件
     *
     * @param string $path 视图文件路径（相对于 view 目录，无需 .php 后缀）
     * @return void
     * @throws RuntimeException 当视图文件不存在时抛出异常
     */
    final protected function render(string $path): void
    {
        $filename  = str_ends_with($path, '.php') ? $path : $path . '.php';
        $view_file = APP_PATH . '/view/' . ltrim($filename, '/');

        // 安全检查：确保路径在 view 目录内
        if (strpos($view_file, APP_PATH . '/view/') !== 0) {
            throw new RuntimeException('非法视图路径');
        }

        if (!is_file($view_file)) {
            throw new RuntimeException("视图文件不存在：{$view_file}");
        }

        include $view_file;
    }

    /**
     * 获取请求数据，并移除首尾空白。
     *
     * @param  string $field_name 请求字段名
     * @param  mixed  $default    默认返回值，当字段不存在时返回此值
     * @return mixed  返回 trim 后的字符串、或默认值
     */
    final public function post(string $field_name, mixed $default = null): mixed
    {
        $value = filter_input(INPUT_POST, $field_name, FILTER_UNSAFE_RAW);

        if ($value === null || $value === false) {
            return $default;
        }

        return trim($value);
    }

    /**
     * 获取或设置 Session 值
     *
     * @return mixed 对应的值、全部 session 数组，或 null
     */
    final protected function session(mixed ...$args): mixed
    {
        return $this->app->session(...$args);
    }

    /**
     * 获取所有 URL 段的数组
     *
     * @return array<int, string>
     */
    final protected function getSegments(): array
    {
        return $this->app->getRouter()->getSegments();
    }

    /**
     * 获取指定索引的 URL 段
     *
     * @param  int $index  段的索引 (从 0 开始)
     * @return string|null 匹配的段字符串，如果不存在则返回 null
     */
    final protected function segment(int $index): ?string
    {
        if ($index < 0) {
            throw new InvalidArgumentException("分段索引不能为负。得到: $index");
        }
        return $this->getSegments()[$index] ?? null;
    }

    /**
     * 安全地获取并转换 URL 段为整数。
     *
     * @param int $index   段索引（从 0 开始）
     * @param int $default 如果段不存在或无效，则返回此默认值
     * @return int
     */
    protected function intSegment(int $index, int $default = 0): int
    {
        $value        = $this->segment($index);
        $validatedInt = filter_var($value, FILTER_VALIDATE_INT);

        return ($validatedInt !== false) ? $validatedInt : $default;
    }

    /**
     * 安全地获取并转换 URL 段为布尔值
     *
     * '1', 'true', 'on', 'yes' 会被视为 true。
     * '0', 'false', 'off', 'no' 会被视为 false。
     * 其他任何值（包括空字符串或无效值）都将返回默认值。
     *
     * @param int  $index   段索引
     * @param bool $default 默认值
     * @return bool
     */
    protected function boolSegment(int $index, bool $default = false): bool
    {
        $value = $this->segment($index);

        if ($value === null || $value === '') {
            return $default;
        }

        $validatedBool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

        return ($validatedBool !== null) ? $validatedBool : $default;
    }

    /**
     * 获取仅包含字母、数字、下划线的段
     * 此方法适合用于需要安全标识符的场景，如文件名、路由参数等。
     *
     * @param int    $index   段索引
     * @param string $default 如果段不存在或清理后为空，则返回此默认值
     * @return string
     */
    protected function alnumSegment(int $index, string $default = ''): string
    {
        $value = $this->segment($index);

        if ($value === null || $value === '') {
            return $default;
        }

        // 使用 preg_replace 清理字符串，移除所有非字母、数字、下划线的字符。
        $cleaned = preg_replace('/[^a-zA-Z0-9_]/', '', $value);

        // 确保清理后的字符串不为空，否则返回默认值。
        return ($cleaned !== '') ? $cleaned : $default;
    }
}
